﻿
    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Reflection;
#if WinRT
    using Windows.UI.Xaml;
	using Windows.UI.Xaml.Data;
    using Windows.UI.Interactivity;
#else
    using System.Windows;
	using System.Windows.Data;
    using System.Windows.Interactivity;
#endif

#if WINDOWS_PHONE
    using Microsoft.Phone.Controls;
#endif


namespace DailyFx.UI
{
	using DailyFx.Common;


    public static class Bind 
	{
        static readonly ILog Log = LogManager.GetLog ( typeof(Bind) );


		/// <summary>
		///		Allows binding on an existing view. 
		///		- Use this on root UserControls, Pages and Windows; not in a DataTemplate.
		/// </summary>
		public static DependencyProperty ModelProperty = DependencyProperty.RegisterAttached
			("Model", typeof(object), typeof(Bind), new PropertyMetadata(null, ModelChanged) );

		/// <summary>
		///   Allows binding on an existing view without setting the data context. 
		///   - Use this from within a DataTemplate.
		/// </summary>
		public static DependencyProperty UncontextModelProperty = DependencyProperty.RegisterAttached
			("UncontextModel", typeof(object), typeof(Bind), new PropertyMetadata(null, UncontextModelChanged ));

		/// <summary>																	
		/// 
		/// </summary>
		internal static DependencyProperty NoContextProperty = DependencyProperty.RegisterAttached
			("NoContext", typeof(bool), typeof(Bind), new PropertyMetadata(false) );


		/// <summary>
		/// 
		/// </summary>
		private static readonly DependencyProperty DataContextProperty = DependencyProperty.RegisterAttached
			("DataContext", typeof(object), typeof(Bind), new PropertyMetadata(null, DataContextChanged) );


		/// <summary>
		///		Allows application of conventions at design-time.
		/// </summary>
		public static DependencyProperty InDesignerProperty = DependencyProperty.RegisterAttached
			("InDesigner", typeof(bool), typeof(Bind), new PropertyMetadata(false, InDesignerChanged) );


		/// <summary>
        ///		Gets the model to bind to.
        /// </summary>
        public static object GetModel( DependencyObject dependencyObject)
		{
            return dependencyObject.GetValue( ModelProperty);
        }

		/// <summary>
		///		Sets the model to bind to.
		/// </summary>
		public static void SetModel ( DependencyObject dependencyObject, object value)
		{
			dependencyObject.SetValue(ModelProperty, value);
		}

        /// <summary>
        ///		Gets the model to bind to .
        /// </summary>
        public static object GetUncontextModel ( DependencyObject dependencyObject) 
		{
            return dependencyObject.GetValue( UncontextModelProperty);
        }

        /// <summary>
        ///		Sets the model to bind to .
        /// </summary>
		public static void SetUncontextModel (DependencyObject dependencyObject, object value)
		{
            dependencyObject.SetValue(UncontextModelProperty, value);
        }

       
       
		/// <summary>
		/// 
		/// </summary>
        static void ModelChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
            if ( Designer.IfTrue || e.NewValue == null || e.NewValue == e.OldValue) 
			{
                return;
            }

            var frameworkElement = d as FrameworkElement;
            if (frameworkElement == null) 
                return;
            

            View.CarryOnViewLoaded( frameworkElement, 
				delegate
				{
					var target = e.NewValue;
					var containerKey = e.NewValue as string;
					if (containerKey != null) {
					    target = IoC.DoGet(null, containerKey);
					}

					d.SetValue(View.IsScopeRootProperty, true);

					var context = string.IsNullOrEmpty(frameworkElement.Name)
					                  ? frameworkElement.GetHashCode().ToString()
					                  : frameworkElement.Name;

					Bind.SetBinding ( target, d, context );
				});
        }

		/// <summary>
		/// 
		/// </summary>
        static void UncontextModelChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e ) 
		{
            if ( Designer.IfTrue || e.NewValue == null || e.NewValue == e.OldValue)
			{
                return;
            }

            var frameworkElement = d as FrameworkElement;
            if (frameworkElement == null) 
                return;

            View.CarryOnViewLoaded ( frameworkElement,
				delegate 
				{
					var target = e.NewValue;
					var key = e.NewValue as string;
					if (key != null)
					{
						target = IoC.DoGet(null, key);
					}

					d.SetValue(View.IsScopeRootProperty, true);

					var context = string.IsNullOrEmpty(frameworkElement.Name)
										? frameworkElement.GetHashCode().ToString()
										: frameworkElement.Name;

					d.SetValue( NoContextProperty, true );
					Bind.SetBinding ( target, d, context );
				});
        }


        static void DataContextChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e) 
		{
            if ( ! Designer.IfTrue )
                return;

            var enable = d.GetValue( InDesignerProperty );
            if (enable == null || ((bool)enable) == false || e.NewValue == null)
                return;

            var frameworkElement = d as FrameworkElement;
            if ( frameworkElement == null )
                return;

            Bind.SetBinding ( e.NewValue, d, string.IsNullOrEmpty(frameworkElement.Name) 
													? frameworkElement.GetHashCode().ToString() 
													: frameworkElement.Name
			);
        }

		
		/// <summary>
        ///		Gets whether or not conventions are being applied at design-time.
        /// </summary>
#if NET
        [AttachedPropertyBrowsableForTypeAttribute(typeof(DependencyObject))]
#endif
        public static bool GetInDesigner ( DependencyObject dependencyObject ) 
		{
            return (bool)dependencyObject.GetValue(InDesignerProperty) ;
        }

		/// <summary>
		///		Sets whether or not do bind conventions at design-time.
		/// </summary>
		public static void SetInDesigner ( DependencyObject dependencyObject, bool value )
		{
			dependencyObject.SetValue( InDesignerProperty, value );
		}

		static void InDesignerChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			if (! Designer.IfTrue )
				return;

			var atDesigning = (bool)e.NewValue;
			if (!atDesigning)
				return;

			BindingOperations.SetBinding( d, DataContextProperty, new Binding() );
		}

        /// <summary>
        ///		Gets or sets a value indicating whether to apply conventions by default.
        /// </summary>
        public static bool ApplyConventionsByDefault = true;

        /// <summary>
        ///		Indicates whether or not the conventions have already been applied to the view.
        /// </summary>
        public static readonly DependencyProperty ConventionsAppliedProperty 
			= DependencyProperty.RegisterAttached ( "ConventionsApplied", typeof(bool), typeof(Bind), null );

        /// <summary>
        ///		Checks whether a view should have conventions applied to it.
        /// </summary>
        public static bool ShouldApplyConventions ( FrameworkElement view) 
		{
            var overriden = View.GetApplyConventions (view);
            return overriden.GetValueOrDefault ( ApplyConventionsByDefault );
        }

        /// <summary>
        ///		Creates data bindings on the view's controls based on the provided properties.
        /// </summary>
        public static Func<IEnumerable<FrameworkElement>, Type, IEnumerable<FrameworkElement>> 
			PropertyBinding = ( namedElements, modelType ) => 
		{
            var unmatchedElements = new List<FrameworkElement>();

            foreach ( var element in namedElements )
			{
                var cleanName = element.Name.Trim('_');
                var parts = cleanName.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);

				var property = TypeExtension.GetProperty (modelType, parts[0], true );
                var modelType2 = modelType;

                for (int i = 1; i < parts.Length && property != null; i++) 
				{
					modelType2 = property.PropertyType;
					property = TypeExtension.GetProperty(modelType2, parts[i], true);
                }

                if (property == null) 
				{
                    unmatchedElements.Add( element);
                    Log.Info("Binding Convention Not Applied: Element {0} did not match a property.", element.Name);
                    continue;
                }

                var convention = ConventionManager.GetConvention( element.GetType() );
                if (convention == null)
				{
                    unmatchedElements.Add(element);
                    Log.Warn( "Binding Convention Not Applied: No conventions configured for {0}.", element.GetType() );
                    continue;
                }

                var applied = convention.ApplyBinding (
					modelType2, cleanName.Replace('_', '.'), property, element, convention );

                if (applied) 
				{
                    Log.Info("Binding Convention Applied: Element {0}.", element.Name);
                }
                else 
				{
                    Log.Info("Binding Convention Not Applied: Element {0} has existing binding.", element.Name);
                    unmatchedElements.Add(element);
                }
            }

            return unmatchedElements;
        };

        /// <summary>
        ///		Attaches instances of <see cref="MessageAction"/> to the view's controls based on the provided methods.
        /// </summary>
        public static Func<IEnumerable<FrameworkElement>, Type, IEnumerable<FrameworkElement>> 
			ActionBinding = (namedElements, modelType) => 
		{
#if WinRT
            var methods = modelType.GetRuntimeMethods();
#else
			var methods = modelType.GetMethods();
#endif
            var unmatchedElements = namedElements.ToList();

            foreach (var method in methods) 
			{
				var foundControl = TreeHelper.FindNamedElement( unmatchedElements, method.Name);
                if (foundControl == null)
				{
                    Log.Info("Action Convention Not Applied: No actionable element for {0}.", method.Name);
                    continue;
                }

                unmatchedElements.Remove( foundControl );

                var triggers = Interaction.GetTriggers( foundControl );
                if (triggers != null && triggers.Count > 0)
				{
                    Log.Info("Action Convention Not Applied: Interaction.Triggers already set on {0}.", foundControl.Name);
                    continue;
                }

                var message = method.Name;
                var parameters = method.GetParameters();

                if  ( parameters.Length > 0 )
				{
                    message += "(";

                    foreach (var parameter in parameters)
					{
                        var paramName = parameter.Name;
                        var specialWords = "$" + paramName.ToLower();

						if (MessageAction.Keywords.ContainsKey( specialWords ))
							paramName = specialWords ;

                        message += paramName + ",";
                    }

                    message = message.Remove ( message.Length - 1, 1 );
                    message += ")";
                }

                Log.Info("Action Convention Applied: Action {0} on element {1}.", method.Name, message);
                MsgProps.SetRegister ( foundControl, message );
            }

            return unmatchedElements;
        };

        /// <summary>
        ///		Allows the developer to add custom handling of named elements which were not matched by any default conventions.
        /// </summary>
        public static Action<IEnumerable<FrameworkElement>, Type> HandleUnmatchedElements = (elements, viewModelType) => { };

        /// <summary>
        /// Binds the specified viewModel to the view.
        /// </summary>
        ///<remarks>Passes the the view model, view and creation context (or null for default) to use in applying binding.</remarks>
        public static Action<object, DependencyObject, object> SetBinding = (viewModel, view, context) =>
		{
#if !WinRT
            // when using d:DesignInstance, Blend tries to assign the DesignInstanceExtension class as the DataContext,
            // so here we get the actual ViewModel which is in the Instance property of DesignInstanceExtension
            if ( Designer.IfTrue )
			{
                var vmType = viewModel.GetType();
                if ( vmType.FullName == "Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension")
				{
                    var propInfo = vmType.GetProperty("Instance", BindingFlags.Instance | BindingFlags.NonPublic);
                    viewModel = propInfo.GetValue(viewModel, null);
                }
            }
#endif

            Log.Info( "Binding {0} and {1}.", view, viewModel );

            if ((bool)view.GetValue( Bind.NoContextProperty) ) 
			{
                ActionProps.SetUncontextTarget ( view, viewModel );
            }
            else {
                ActionProps.SetTarget (view, viewModel);
            }

            var viewAware = viewModel as IViewAware;
            if (viewAware != null) {
                Log.Info("Attaching {0} to {1}.", view, viewAware);
                viewAware.AttachView( view, context);
            }

            if ((bool)view.GetValue(ConventionsAppliedProperty))
			{
                return;
            }

            var element = View.GetFirstNonCreatedView(view) as FrameworkElement;
            if (element == null)
			{
                return;
            }

#if WINDOWS_PHONE
			// Bind the AppBar
			//
			var page = view as PhoneApplicationPage ;
            if ( page != null && page.ApplicationBar == null )
			{
                var triggers = Interaction.GetTriggers(view);

				foreach( var btn in page.ApplicationBar.Buttons ) 
				{
					var button = btn as IAppBarActionMessage;
					if ( button == null || string.IsNullOrEmpty(button.Message) )
					{
						continue;
					}

					var parsedTrigger = Parser.Parse(view, button.Message).First();
					var trigger = new AppBarItemTrigger(button);
					var messageActions = parsedTrigger.Actions.OfType<ActionMessage>().ToList();
					messageActions.Apply( x => 
					{
						x.applicationBarSource = button;
						parsedTrigger.Actions.Remove(x);
						trigger.Actions.Add(x);
					});
                
					triggers.Add(trigger);
				}

				foreach ( var item in page.ApplicationBar.MenuItems)
				{
					var menuItem = item as IAppBarActionMessage;
					if (menuItem == null || string.IsNullOrEmpty(menuItem.Message)) 
					{
						continue;
					}

					var parsedTrigger = Parser.Parse(view, menuItem.Message).First();
					var trigger = new AppBarItemTrigger(menuItem);
					var messageActions = parsedTrigger.Actions.OfType<ActionMessage>().ToList();
					messageActions.Apply( x =>
					{
						x.applicationBarSource = menuItem;
						parsedTrigger.Actions.Remove(x);
						trigger.Actions.Add(x);
					});

					triggers.Add(trigger);
				}
            }
#endif

			if ( !ShouldApplyConventions(element) )
			{
                Log.Info("Skipping conventions for {0} and {1}.", element, viewModel );
#if WINDOWS_PHONE
                view.SetValue( ConventionsAppliedProperty, true ); // we always apply the AppBar conventions
#endif
                return;
            }

            var vmTypeInfo = viewModel.GetType();
#if SL5 || NET45
            var vmTypeProvider = viewModel as ICustomTypeProvider;
			if ( vmTypeProvider != null )
			{
                vmTypeInfo = vmTypeProvider.GetCustomType ();
            }
#endif
            var namedElements = TreeHelper.GetNamedChildren ( element);

#if SILVERLIGHT
            namedElements.Apply( x => x.SetValue( View.IsLoadedProperty, element.GetValue(View.IsLoadedProperty)) );
#endif
            namedElements = ActionBinding(namedElements, vmTypeInfo );
            namedElements = PropertyBinding(namedElements, vmTypeInfo );
            
			// what if unmatched elements ?
			HandleUnmatchedElements( namedElements, vmTypeInfo );

			// conventions applied .
            view.SetValue(ConventionsAppliedProperty, true);
        };


    }
}
